import { CsaUtils, csa } from "./utils";

/** 
 * Csa从站访问器 
 * @author lester<lieyunwuhe@sina.com>
 * @date 2022年12月03日
 */
class CsaFrameAccessor {
  /** 监听消息队列 */
  private readonly listens: { [key: csa.CsaFrameEvent]: { once: boolean, listener: csa.CsaFrameListener }[] } = {};
  /** 监听调用函数队列 */
  private readonly returnHandlers: Record<string, Function> = {};
  /** 导出上下文 */
  private exposeCtx: Record<csa.CsaFrameCsaCallName, Function> = {};
  /** 消息发送端 */
  private port!: MessagePort;

  /** 设置导出函数 */
  set expose(ctx: Record<string, Function>) {
    this.exposeCtx = ctx || {};
  }
  /** 是否已连接成功/或准备好了 */
  get connected() {
    return !!this.port;
  }

  constructor() {
    const onMessage = async (event: MessageEvent) => {
      if (this.port) return console.warn('[CSA/FRAME/MESSAGE]ALREADY');
      if (event.ports.length != 1) return;
      if (typeof event.data !== "object") return;
      if (event.data === 'connecting') return;

      this.port = event.ports[0];
      removeEventListener('message', onMessage);

      this.port.onmessage = async (event: MessageEvent<csa.CsaMessage>) => {
        const { csaEvent } = event.data;
        // 从站调用主站返回值回调
        if (CsaUtils.EVENT_CALL_RETURN === csaEvent) {
          const { csaId, csaIsError, csaReturnValue } = event.data;
          return CsaUtils.nextTick(() => {
            if (typeof csaId !== 'string') return;

            const handler = this.returnHandlers[csaId];
            if (typeof handler !== 'function') return;
            const value = csaIsError ? new Error(`[call-error]` + csaReturnValue)
              : csaReturnValue;
            handler(value);
          });
        }

        // 主站调用从站
        if (CsaUtils.EVENT_CALL === csaEvent) {
          const { csaId, csaFnName, csaFnArgs } = event.data;
          return CsaUtils.nextTick(async () => {
            const method = this.exposeCtx[csaFnName];
            try {
              const value = await method(...csaFnArgs);
              const message: csa.CsaCallReturnMessage = {
                csaSource: CsaUtils.SOURCE_FRAME,
                csaOrigin: location.origin,
                csaEvent: CsaUtils.EVENT_CALL_RETURN,
                csaId,
                csaReturnValue: value
              };
              this.port.postMessage(message);
            } catch (error) {
              const message: csa.CsaCallReturnMessage = {
                csaSource: CsaUtils.SOURCE_FRAME,
                csaOrigin: location.origin,
                csaEvent: CsaUtils.EVENT_CALL_RETURN,
                csaId,
                csaIsError: true,
                csaReturnValue: (error as any).message
              };
              this.port.postMessage(message);
            }
          });
        }

        if (CsaUtils.EVENT_EMIT === csaEvent) {
          const { csaEmitName, csaEmitArgs } = event.data;
          CsaUtils.nextTick(() => {
            const listens = (this.listens[csaEmitName] || []);
            for (const listen of listens) {
              CsaUtils.tryApply(listen.listener, csaEmitArgs, listen);
            }
          });
          return;
        }
        console.warn(`[CSA/FRAME/MESSAGE]NOT SUPPORT`, event);
      }

      CsaUtils.nextTick(() => {
        const listens = (this.listens.ready || []);
        for (const listen of listens) {
          CsaUtils.tryApply(listen.listener);
        }
        this.port.postMessage("connected");
      });
    }
    addEventListener('message', onMessage);

    Object.defineProperties(this, {
      listens: { configurable: false, enumerable: false, writable: false },
      returnHandlers: { configurable: false, enumerable: false, writable: false },
      exposeCtx: { configurable: false, enumerable: false, writable: true },
      port: { configurable: false, enumerable: false, writable: true },
    })
  }

  /**
   * 调用主站导出方法
   * @param name 主站导出方法名称
   * @param args 主站导出方法参数值
   * @returns 主站导出方法调用返回值
   */
  async call<T = any | void>(name: csa.CsaMasterCsaCallName, ...args: any[]): Promise<T> {
    if (!this.connected) throw new Error('Csa 未连接');
    return new Promise<T>(resolve => {
      const message: csa.CsaCallMessage = {
        csaId: `${name}_${new Date().getTime().toString(16)}`,
        csaSource: CsaUtils.SOURCE_FRAME,
        csaOrigin: location.origin,
        csaEvent: CsaUtils.EVENT_CALL,
        csaFnName: name,
        csaFnArgs: args
      };
      // 添加返回函数
      this.returnHandlers[message.csaId!] = resolve;
      this.port.postMessage(message);
    });
  }

  /**
   * 发送/触发从站消息
   * @param destination 发送/触发目标
   * @param messagePayload 消息负载信息
   */
  emit(destination: csa.CsaMasterFrameEvent, ...messagePayload: any[]) {
    if (!this.connected) throw new Error('Csa 未连接');
    const message: csa.CsaEmitMessage = {
      csaSource: CsaUtils.SOURCE_FRAME,
      csaOrigin: location.origin,
      csaEvent: CsaUtils.EVENT_EMIT,
      csaEmitName: destination,
      csaEmitArgs: messagePayload
    };
    this.port.postMessage(message);
  }

  /**
   * 监听/订阅主站消息
   * @param destination 订阅/监听目标
   * @param listener  订阅/监听消息处理函数
   * @param once 是否一次性，默认false,永久监听
   * @returns 停止订阅/监听处理函数
   */
  on(type: csa.CsaFrameEvent, listener: csa.CsaFrameListener, once = false): csa.CsaStopFrameListenHandler {
    if (this.connected) {
      Promise.resolve(() => listener())
      if (once) return () => { };
    }
    const listen = { once, listener };
    return () => {
      const index = this.listens[type].indexOf(listen);
      if (index >= 0) this.listens[type].splice(index, 0);
    }
  }

  /**
   * 监听Csa是否已加载[允许多次监听]
   */
  ready(): Promise<void>;
  /**
   * 监听Csa是否已加载[允许多次监听]
   * @param handler 已加载的处理函数
   */
  ready(handler: csa.FrameReadyListener): void;
  ready(handler?: csa.FrameReadyListener): void | Promise<void> {
    if (typeof handler === 'undefined') {
      return new Promise<void>(resolve => this.on('ready', resolve));
    }
    this.on('ready', handler, true);
  }


  /** 创建Csa跨站-从站对象 */
  public static create() {
    return new CsaFrameAccessor();
  }

}

export default CsaFrameAccessor;
export { CsaFrameAccessor };
export const createFrameAccessor = CsaFrameAccessor.create;

